# -*- coding: utf-8 -*-

import os
import shutil
import cPickle as pickle

import Image
from django.db.models import ImageField, signals, ManyToManyField, Field, BLANK_CHOICE_DASH, CharField
from django.dispatch import dispatcher
from django.conf import settings
from django.utils.translation import ugettext_lazy, string_concat
from django.utils.encoding import smart_unicode
from django.utils.functional import curry


__all__ = [
    "ImprovedImageField",
    "StateField",
    "PickledManyToManyField",
    ]


def fit(file_path, max_width, max_height):
    """Resize file (located on file path) to maximum dimensions proportionally.
    At least one of max_width and max_height must be not None."""
    if not (max_width or max_height):
        # Can't resize
        return
    img = Image.open(file_path)
    w, h = img.size
    w = int(max_width or w)
    h = int(max_height or h)
    img.thumbnail((w, h), Image.ANTIALIAS)
    img.save(file_path)


def rename(old_path, new_name):
    """
    old_name is relative to MEDIA_ROOT
    new_name is just base name, without extension
    """
    def fp(path):
        return os.path.join(settings.MEDIA_ROOT, path)
    if not os.path.isfile(fp(old_path)):
        return old_path
    path = os.path.dirname(old_path)
    ext = os.path.splitext(old_path)[1]
    # django wants to have '/' in path
    new_path = '/'.join([path, new_name + ext])
    if new_path != old_path:
        try:
            shutil.move(fp(old_path), fp(new_path))
        except IOError:
            return old_path
    return new_path


class ImprovedImageField(ImageField):
    """Allows model instance to specify following parameters dynamically:

     - upload_to, specify following method for model:

        def get_FIELD_upload_to(self):
            return 'avatars/%d' % self.user.username

     - filename (relative to upload_to, without extension):

         def get_FIELD_filename(self):
             return self.pk

    Additionally field supports automatic resizing, if at least one of
    max_width and max_height supplied.

    Current flaws: upload_to must be specified as parameter (even if
    custom method exist, although parameter can carry useless value).

    Based on:
        http://code.djangoproject.com/wiki/CustomUploadAndFilters
        http://scottbarnham.com/blog/2007/07/31/uploading-images-to-a-dynamic-path-with-django/

    Copyright (c) 2008 Alexander Solovyov under new BSD License.
    """
    def __init__(self, max_width=None, max_height=None, **kwargs):
        self.max_width, self.max_height = max_width, max_height
        super(ImprovedImageField, self).__init__(**kwargs)

    def db_type(self):
        """Required by Django for ORM."""
        return 'varchar(100)'

    def contribute_to_class(self, cls, name):
        """Hook up events so we can access the instance."""
        super(ImprovedImageField, self).contribute_to_class(cls, name)
        dispatcher.connect(self._upload_to, signals.post_init, sender=cls)
        dispatcher.connect(self._resize, signals.post_save, sender=cls)
        dispatcher.connect(self._rename, signals.pre_save, sender=cls)

    def _upload_to(self, instance=None):
        """Get dynamic upload_to value from the model instance."""
        if hasattr(instance, 'get_%s_upload_to' % self.attname):
            self.upload_to = getattr(instance, 'get_%s_upload_to' % self.attname)()

    def _resize(self, instance):
        if getattr(instance, self.attname):
            real_path = os.path.join(settings.MEDIA_ROOT, getattr(instance, self.attname))
            if os.path.isfile(real_path):
                fit(real_path, self.max_width, self.max_height)

    def _rename(self, instance):
        if hasattr(instance, 'get_%s_filename' % self.attname) and getattr(instance, self.attname):
            filename = getattr(instance, 'get_%s_filename' % self.attname)()
            new_path = rename(getattr(instance, self.attname), filename)
            setattr(instance, self.attname, new_path)


def state_STATE(self, state, save=True):
    self.state = state
    if save:
        self.save()

class StateField(CharField):
    STANDARD = 's'
    READ_ONLY = 'ro'
    HIDDEN = 'h'
    DELETED = 'd'

    STATES = ((STANDARD, 'Standard'), (READ_ONLY, 'Read only'), (HIDDEN, 'Hidden'), (DELETED, 'Deleted'))

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 2
        kwargs['db_index'] = True
        kwargs['choices'] = StateField.STATES
        kwargs['default'] = StateField.STANDARD
        super(StateField, self).__init__(*args, **kwargs)

    def contribute_to_class(self, cls, name):
        setattr(cls, '%s_standard' % name, curry(state_STATE, state=StateField.STANDARD))
        setattr(cls, '%s_read_only' % name, curry(state_STATE, state=StateField.READ_ONLY))
        # this piece of code responds for hidden/deleted records
#         setattr(cls, '%s_hidden' % name, curry(state_STATE, state=StateField.HIDDEN))
#         setattr(cls, '%s_deleted' % name, curry(state_STATE, state=StateField.DELETED))
#         def readable_or_404(self, user):
#             if not user.is_staff:
#                 self_state = getattr(self, name)
#                 if self_state != StateField.STANDARD and self_state != StateField.READ_ONLY:
#                     raise Http404
#             return True
#         setattr(cls, 'readable_or_404', readable_or_404)
        super(StateField, self).contribute_to_class(cls, name)

    def db_type(self):
        return 'varchar(%d)' % self.max_length


class PickledManyToManyField(Field):
    def __init__(self, to, to_field=None, related_name=None, **kwargs):
        to_field = to_field or to._meta.pk.name
        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
        self.my_rel = PickledManyToManyRel(to, to_field,
            num_in_admin=kwargs.pop('num_in_admin', 0),
            filter_interface=kwargs.pop('filter_interface', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None),
            raw_id_admin=kwargs.pop('raw_id_admin', False))
        if self.my_rel.raw_id_admin:
            kwargs.setdefault('validator_list', []).append(self.isValidIDList)
        super(PickledManyToManyField, self).__init__(**kwargs)

        if self.my_rel.raw_id_admin:
            msg = ugettext_lazy('Separate multiple IDs with commas.')
        else:
            msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
        self.help_text = string_concat(self.help_text, ' ', msg)

    def get_attname(self):
        return '%s_%ss' % (self.name, self.my_rel.to._meta.pk.name)

    def set_attributes_from_rel(self):
        self.name = self.name or self.get_attname()
        self.verbose_name = self.verbose_name or self.my_rel.to._meta.verbose_name
        self.my_rel.field_name = self.my_rel.field_name or self.my_rel.to._meta.pk.name

    def contribute_to_class(self, cls, name):
        super(PickledManyToManyField, self).contribute_to_class(cls, name)
        self.set_attributes_from_rel()
        self.related_query_name = lambda self: opts.object_name.lower()
        setattr(cls, self.name, PickledManyRelatedObjectDescriptor(self))
        standard_init = cls.__init__
        def unpickling_init(slf, *args, **kwargs):
            standard_init(slf, *args, **kwargs)
            value = getattr(slf, self.attname)
            if not value:
                value = set()
            elif not isinstance(value, set):
                # python 2.5.2 can't unpickle unicode argument
                value = pickle.loads(str(value))
                setattr(slf, self.attname, value)
        cls.__init__ = unpickling_init

    def db_type(self):
        return 'text'

    def to_python(self, value):
        return pickle.loads(value)

    def get_db_prep_save(self, value):
        return pickle.dumps(value)

    def value_from_object(self, object):
        return getattr(object, self.name).all()

    ###############################################
    # this is just copy-pasted from ManyToManyField
    def get_manipulator_field_objs(self):
        from django import oldforms
        if self.my_rel.raw_id_admin:
            return [oldforms.RawIdAdminField]
        else:
            choices = self.get_choices_default()
            return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]

    def get_choices_default(self):
        return self.get_choices(include_blank=False)

    def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
        "Returns a list of tuples used as SelectField choices for this field."
        first_choice = include_blank and blank_choice or []
        if self.choices:
            return first_choice + list(self.choices)
        rel_model = self.my_rel.to
        if hasattr(self.my_rel, 'get_related_field'):
            lst = [(getattr(x, self.my_rel.get_related_field().attname), smart_unicode(x)) for x in rel_model._default_manager.complex_filter(self.my_rel.limit_choices_to)]
        else:
            lst = [(x._get_pk_val(), smart_unicode(x)) for x in rel_model._default_manager.complex_filter(self.my_rel.limit_choices_to)]
        return first_choice + lst

    def isValidIDList(self, field_data, all_data):
        "Validates that the value is a valid list of foreign keys"
        mod = self.my_rel.to
        try:
            pks = map(int, field_data.split(','))
        except ValueError:
            # the CommaSeparatedIntegerField validator will catch this error
            return
        objects = mod._default_manager.in_bulk(pks)
        if len(objects) != len(pks):
            badkeys = [k for k in pks if k not in objects]
            raise validators.ValidationError, ungettext("Please enter valid %(self)s IDs. The value %(value)r is invalid.",
                    "Please enter valid %(self)s IDs. The values %(value)r are invalid.", len(badkeys)) % {
                'self': self.verbose_name,
                'value': len(badkeys) == 1 and badkeys[0] or tuple(badkeys),
            }

    # end of copy-past
    ##################



class PickledManyRelatedObjectDescriptor(object):
    def __init__(self, field_with_rel):
        self.field = field_with_rel

    def __get__(self, instance, instance_type=None):
        if instance is None:
            raise AttributeError, "%s must be accessed via instance" % self.field.name

        val = getattr(instance, self.field.attname)

        rel_model = self.field.my_rel.to
        superclass = rel_model._default_manager.__class__
        RelatedManager = create_pickled_many_related_manager(superclass)

        manager = RelatedManager(
            model=rel_model,
            core_filters={'%s__in' % self.field.my_rel.field_name: val},
            instance=instance,
            attname=self.field.attname,
            )

        return manager

    def __set__(self, instance, value):
        if instance is None:
            raise AttributeError, "%s must be accessed via instance" % self._field.name
        # Set the value of the related field
        val = set()
        for obj in value:
            try:
                val.add(getattr(obj, self.field.my_rel.get_related_field().attname))
            except AttributeError:
                pass
        setattr(instance, self.field.attname, val)

        # Clear the cache, if it exists
        try:
            delattr(instance, self.field.get_cache_name())
        except AttributeError:
            pass


class PickledManyToManyRel(object):
    def __init__(self, to, field_name, num_in_admin=0, filter_interface=None, limit_choices_to=None, raw_id_admin=False):
        self.to = to
        self.field_name = field_name
        self.num_in_admin = num_in_admin
        self.filter_interface = filter_interface
        if limit_choices_to is None:
            limit_choices_to = {}
        self.limit_choices_to = limit_choices_to
        self.edit_inline = False
        self.raw_id_admin = raw_id_admin
        self.multiple = True

        self.related_name = '%s_%s_set' % (to._meta.pk.name, field_name)

        assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"

    # get_related_field = ManyToOneRel.get_related_field
    def get_related_field(self):
        "Returns the Field in the 'to' object to which this relationship is tied."
        return self.to._meta.get_field(self.field_name)


def create_pickled_many_related_manager(superclass):
    class RelatedManager(superclass):
        def __init__(self, model=None, core_filters=None, instance=None, attname=None):
            super(RelatedManager, self).__init__()
            self.model = model
            self.core_filters = core_filters
            self.instance = instance
            self.attname = attname

        def get_query_set(self):
            return superclass.get_query_set(self).filter(**(self.core_filters))

        def add(self, *objs):
            obj_pks = (obj.pk for obj in objs)
            setattr(self.instance, self.attname, getattr(self.instance, self.attname).union(obj_pks))
        add.alters_data = True

        def remove(self, *objs):
            obj_pks = (obj.pk for obj in objs)
            setattr(self.instance, self.attname, getattr(self.instance, self.attname).difference(obj_pks))
        remove.alters_data = True

        def clear(self):
            setattr(self.instance, self.attname, set())
        clear.alters_data = True

        def create(self, **kwargs):
            new_obj = self.model(**kwargs)
            new_obj.save()
            self.add(new_obj)
            return new_obj
        create.alters_data = True

        def __eq__(self, other):
            try:
                return getattr(self.instance, self.attname) == getattr(other.instance, other.attname)
            except AttributeError:
                return False

        def __nonzero__(self):
            return bool(getattr(self.instance, self.attname))

    return RelatedManager
